package org.jspresso.framework.model.entity;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.ObjectUtils;
import org.jspresso.framework.model.component.IComponent;
import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IComponentDescriptor;
import org.jspresso.framework.model.descriptor.IModelDescriptorAware;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor;
import org.jspresso.framework.model.descriptor.IRelationshipEndPropertyDescriptor;
import org.jspresso.framework.util.accessor.ICollectionAccessor;

public class SmarterEntityCloneFactory extends SmartEntityCloneFactory {
	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <E extends IComponent> E cloneComponent(E componentToClone, IEntityFactory entityFactory) {
		E clonedComponent = (E) entityFactory.createComponentInstance(componentToClone.getComponentContract());
		handleRelationships(componentToClone, clonedComponent, entityFactory);
		return clonedComponent;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <E extends IEntity> E cloneEntity(E entityToClone, IEntityFactory entityFactory) {
		E clonedEntity = (E) entityFactory.createEntityInstance(entityToClone.getComponentContract());
		handleRelationships(entityToClone, clonedEntity, entityFactory);
		return clonedEntity;
	}

	private <E extends IComponent> void handleRelationships(E componentToClone,	E clonedComponent, IEntityFactory entityFactory) {
		IComponentDescriptor<?> componentDescriptor = entityFactory.getComponentDescriptor(componentToClone.getComponentContract());

		Map<Object, ICollectionPropertyDescriptor<?>> collRelToUpdate = new HashMap<>();

		for (Map.Entry<String, Object> propertyEntry : componentToClone.straightGetProperties().entrySet()) {
			if (propertyEntry.getValue() != null && !(IEntity.ID.equals(propertyEntry.getKey())|| IEntity.VERSION.equals(propertyEntry.getKey()) || componentDescriptor.getUnclonedProperties().contains(propertyEntry.getKey()))) {

				IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(propertyEntry.getKey());

				if (propertyDescriptor instanceof IRelationshipEndPropertyDescriptor) { // 1-1, 1-n, n-1 or n-n relationship, bidirectional or not

					if (propertyEntry.getValue() instanceof IComponent	&& !(propertyEntry.getValue() instanceof IEntity)) { // Component relationship, we just clone the component (not a real relation in term of DB)
						clonedComponent.straightSetProperty(propertyEntry.getKey(),	cloneComponent((IComponent) propertyEntry.getValue(), entityFactory));
					} 
					else {

						IRelationshipEndPropertyDescriptor reverseDescriptor = ((IRelationshipEndPropertyDescriptor) propertyDescriptor).getReverseRelationEnd();

						if (propertyDescriptor instanceof IReferencePropertyDescriptor<?>) { // 1-1 or n-1 relationship, bidirectional or not
							if (!(reverseDescriptor instanceof IReferencePropertyDescriptor<?>)) { // 1-1 or n-1 relationship, non-bidirectional
								if (((IRelationshipEndPropertyDescriptor) propertyDescriptor).isComposition()) { // We can clone the referenced entity
									clonedComponent.straightSetProperty(propertyEntry.getKey(), cloneEntity((IEntity) propertyEntry.getValue(),	entityFactory));
								} else { // We can copy the reference
									clonedComponent.straightSetProperty(propertyEntry.getKey(),	propertyEntry.getValue());
									if (reverseDescriptor instanceof ICollectionPropertyDescriptor<?>) { // Reverse is a collection, keep in mind we have to update the reverse col
										if (isInitialized(propertyEntry.getValue())) {
											collRelToUpdate.put(propertyEntry.getValue(), (ICollectionPropertyDescriptor<?>) reverseDescriptor);
										}
									}
								}
							}
						} else if (propertyDescriptor instanceof ICollectionPropertyDescriptor<?>) { // 1-n and n-n relationship, bidirectional or not
							// We must force initialization of the collection. So do a get.
							try {
								entityFactory.getAccessorFactory().createPropertyAccessor(propertyEntry.getKey(), componentToClone.getComponentContract()).getValue(componentToClone);
							} catch (IllegalAccessException | NoSuchMethodException ex) {
								throw new EntityException(ex);
							} catch (InvocationTargetException ex) {
								if (ex.getCause() instanceof RuntimeException) {
									throw (RuntimeException) ex.getCause();
								}
								throw new EntityException(ex.getCause());
							}
							
							if(reverseDescriptor == null) { // Non-bidirectional 1-n or n-n relationship
								clonedComponent.straightSetProperty(propertyEntry.getKey(),	propertyEntry.getValue());
							}
							else if (reverseDescriptor instanceof ICollectionPropertyDescriptor<?>) { // Bi-directional 1-n or n-n relationship, let the next part of the method handle the collection initialization
								for (Object reverseCollectionElement : (Collection<?>) propertyEntry.getValue()) {
									if (isInitialized(reverseCollectionElement)) {
										collRelToUpdate.put(reverseCollectionElement, (ICollectionPropertyDescriptor<?>) reverseDescriptor);
									}
								}
							}
						}
					}
				} else {
					clonedComponent.straightSetProperty(propertyEntry.getKey(), ObjectUtils.cloneIfPossible(propertyEntry.getValue()));
				}
			}
		}
		for (Map.Entry<Object, ICollectionPropertyDescriptor<?>> collectionEntry : collRelToUpdate.entrySet()) {
			ICollectionPropertyDescriptor<?> collectionDescriptor = collectionEntry.getValue();
			Class<?> masterContract = null;
			if (collectionDescriptor.getReverseRelationEnd() instanceof IReferencePropertyDescriptor<?>) {
				masterContract = ((IReferencePropertyDescriptor<?>) collectionDescriptor.getReverseRelationEnd()).getReferencedDescriptor().getComponentContract();
			} else if (collectionDescriptor.getReverseRelationEnd() instanceof ICollectionPropertyDescriptor<?>) {
				masterContract = ((ICollectionPropertyDescriptor<?>) collectionDescriptor.getReverseRelationEnd()).getReferencedDescriptor().getElementDescriptor().getComponentContract();
			}
			ICollectionAccessor collectionAccessor = entityFactory.getAccessorFactory().createCollectionPropertyAccessor(collectionDescriptor.getName(), masterContract, clonedComponent.getComponentContract());
			if (collectionAccessor instanceof IModelDescriptorAware) {
				((IModelDescriptorAware) collectionAccessor).setModelDescriptor(collectionDescriptor);
			}
			try {
				Collection<?> existingCollection = collectionAccessor.getValue(collectionEntry.getKey());
				if (!existingCollection.contains(clonedComponent)) {
					// it could already be there through lifecycle handlers / property
					// processors.
					collectionAccessor.addToValue(collectionEntry.getKey(),	clonedComponent);
				}
			} catch (IllegalAccessException | NoSuchMethodException ex) {
				throw new EntityException(ex);
			} catch (InvocationTargetException ex) {
				if (ex.getCause() instanceof RuntimeException) {
					throw (RuntimeException) ex.getCause();
				}
				throw new EntityException(ex.getCause());
			}
		}
	}
}
